package cn.sciento.starter.tenant.custom.core;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import cn.sciento.core.convertor.ApplicationContextHelper;
import cn.sciento.core.exception.CommonException;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.MultiKeyMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import cn.sciento.starter.tenant.custom.annotation.TenantCustom;
import cn.sciento.starter.tenant.custom.annotation.TenantCustomPoint;
import cn.sciento.starter.tenant.custom.repository.TenantCustomPointRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;

public class DynamicTenantCustomBeanProxyFactory {
  private static final Logger LOGGER = LoggerFactory.getLogger(DynamicTenantCustomBeanProxyFactory.class);
  
  private static final String DEFAULT_CODE = "x-default";
  
  public static Object createCustomBeanProxy(Class<?> targetClass, String[] proxyBeanNames) {
    LOGGER.info("dynamic create tenant custom bean proxy, targetClass=[{}], proxyBeanNames={}", targetClass, proxyBeanNames);
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setInterfaces(new Class[] { targetClass });
    proxyFactory.addAdvice((Advice)new DynamicServiceInterceptor(proxyBeanNames));
    return proxyFactory.getProxy();
  }
  
  static class DynamicServiceInterceptor implements MethodInterceptor {
    private final MultiKeyMap methodPointMap = new MultiKeyMap();
    
    private final Map<String, Object> pointBeanMap = new HashMap<>(16);
    
    private TenantCustomPointRepository tenantCustomPointRepository;
    
    private String serviceName;
    
    private String[] proxyBeanNames;
    
    private boolean init;
    
    public DynamicServiceInterceptor(String[] proxyBeanNames) {
      this.proxyBeanNames = proxyBeanNames;
    }
    
    public Object invoke(MethodInvocation invocation) throws Throwable {
      try {
        Method method = invocation.getMethod();
        return method.invoke(getTarget(method), invocation.getArguments());
      } catch (InvocationTargetException e) {
        throw e.getTargetException();
      } 
    }
    
    private Object getTarget(Method method) {
      if (!this.init)
        synchronized (this.methodPointMap) {
          initCustomBeanMap(this.proxyBeanNames);
        }  
      String tenantNum = TenantCustomContext.getTenantNum();
      DynamicTenantCustomBeanProxyFactory.LOGGER.debug("Get actual bean, tenantNum=[{}], methodName=[{}]", tenantNum, method.getName());
      if (StringUtils.isBlank(tenantNum)) {
        DynamicTenantCustomBeanProxyFactory.LOGGER.debug("Get actual bean, tenantNum is blank, use default bean");
        return this.pointBeanMap.get("x-default");
      } 
      Set<String> pointCodes = (Set<String>)this.methodPointMap.get(method.getName(), getMethodParameterizedType(method));
      if (CollectionUtils.isEmpty(pointCodes)) {
        DynamicTenantCustomBeanProxyFactory.LOGGER.debug("Get actual bean, method not custom, use default bean");
        return this.pointBeanMap.get("x-default");
      } 
      String customPointCode = getHighestPriorityPointCode(tenantNum, this.serviceName, pointCodes);
      if (StringUtils.isBlank(customPointCode)) {
        DynamicTenantCustomBeanProxyFactory.LOGGER.debug("Get actual bean, tenant has no authority to custom point, use default bean. allCustomPoint={}", pointCodes);
        return this.pointBeanMap.get("x-default");
      } 
      Object bean = this.pointBeanMap.get(customPointCode);
      DynamicTenantCustomBeanProxyFactory.LOGGER.debug("Get actual bean, tenant has authority to custom point, use custom bean. customPoint={}, customBeanName={}", customPointCode, bean
          .getClass().getSimpleName());
      return bean;
    }
    
    private String getMethodParameterizedType(Method method) {
      Parameter[] parameters = method.getParameters();
      if (ArrayUtils.isEmpty((Object[])parameters))
        return "null"; 
      StringBuilder types = new StringBuilder();
      for (Parameter parameter : parameters)
        types.append(parameter.getParameterizedType().getTypeName()).append(","); 
      return types.toString();
    }
    
    private String getHighestPriorityPointCode(String tenantNum, String serviceName, Set<String> pointCodes) {
      Double highest = null;
      String customPointCode = null;
      for (String pointCode : pointCodes) {
        Double priority = this.tenantCustomPointRepository.getPriority(tenantNum, serviceName, pointCode);
        if (priority == null)
          continue; 
        if (null == highest) {
          highest = priority;
          customPointCode = pointCode;
          continue;
        } 
        if (priority.doubleValue() < highest.doubleValue()) {
          highest = priority;
          customPointCode = pointCode;
        } 
      } 
      return customPointCode;
    }
    
    private void initCustomBeanMap(String[] proxyBeanNames) {
      if (this.init)
        return; 
      DynamicTenantCustomBeanProxyFactory.LOGGER.info("start init tenant custom bean to custom point relation");
      this.tenantCustomPointRepository = (TenantCustomPointRepository)ApplicationContextHelper.getContext().getBean(TenantCustomPointRepository.class);
      this.serviceName = ((Environment) ApplicationContextHelper.getContext().getBean(Environment.class)).getProperty("spring.application.name");
      for (String beanName : proxyBeanNames) {
        Object bean = ApplicationContextHelper.getContext().getBean(beanName);
        TenantCustom tenantCustom = AnnotationUtils.findAnnotation(bean.getClass(), TenantCustom.class);
        if (tenantCustom == null) {
          DynamicTenantCustomBeanProxyFactory.LOGGER.info("standard bean set as default, beanName is [{}]", beanName);
          this.pointBeanMap.put("x-default", bean);
        } else {
          Method[] methods = MethodUtils.getMethodsWithAnnotation(bean.getClass(), TenantCustomPoint.class, true, true);
          if (ArrayUtils.isEmpty((Object[])methods)) {
            DynamicTenantCustomBeanProxyFactory.LOGGER.info("custom method is empty, beanName is [{}]", beanName);
          } else {
            for (Method method : methods) {
              TenantCustomPoint tenantCustomPoint = (TenantCustomPoint)MethodUtils.getAnnotation(method, TenantCustomPoint.class, true, true);
              String pointCode = this.serviceName + "." + tenantCustom.value() + "." + tenantCustomPoint.value();
              if (this.pointBeanMap.containsKey(pointCode))
                throw new CommonException("hiam.error.tenantCustom.duplicatePoint", pointCode);
              this.pointBeanMap.put(pointCode, bean);
              String methodName = method.getName();
              String types = getMethodParameterizedType(method);
              DynamicTenantCustomBeanProxyFactory.LOGGER.info("tenant custom bean: customPoint=[{}], beanName=[{}], methodName=[{}], parameterTypes=[{}]", new Object[] { pointCode, beanName, methodName, types });
              Set<String> pointCodes = (Set<String>) Optional.ofNullable(this.methodPointMap.get(methodName, types)).orElse(new HashSet<String>(16));
              pointCodes.add(pointCode);
              this.methodPointMap.put(methodName, types, pointCodes);
            } 
          } 
        } 
      } 
      this.init = true;
      DynamicTenantCustomBeanProxyFactory.LOGGER.info("init tenant custom point finish");
    }
  }
}
